package models;

import static akka.pattern.Patterns.ask;
import static java.util.concurrent.TimeUnit.SECONDS;

import org.apache.commons.codec.binary.BinaryCodec;

import models.Croupier;
import models.Player;
import models.GameException;
import models.Hand;
import models.msgs.ActionRequest;
import models.msgs.AfterMoveInfo;
import models.msgs.CardInfo;
import models.msgs.CoinsInfo;
import models.msgs.Info;
import models.msgs.Join;
import models.msgs.MovePermission;
import models.msgs.Number;
import models.msgs.Quit;
import models.msgs.WinnerInfo;
import play.libs.Akka;
import play.mvc.WebSocket;
import scala.concurrent.Await;
import scala.concurrent.duration.Duration;
import akka.actor.ActorRef;
import akka.actor.Props;
import akka.actor.UntypedActor;

import com.fasterxml.jackson.databind.JsonNode;

public class Table extends UntypedActor {
	static int numberOfPlayers;
	static int startingCoins;
	static int smallBlind;
	static int bigBlind;
	static int maxBet;
	static int stake = 0;
	static int round = 1;
	static Deck2 deck;
	static int whoDealer;
	static int whoseTurn;
	private static int playerWithSmallBlind;
	static int moveCounter = 1; // liczba wykonanych ruchów w rundzie
	static int playersExchangedCardsCounter = 0;
	static int playersFoldedCounter = 0;
	static int playersInGameCounter;
	static int playersBeingInGameInActualRound;
	static boolean isBet = false;

	static int sitingPlayersCounter = 0;

	static ActorRef tableActor;
	static ActorRef[] playersActors;
	static Player[] players;

	public Table(int numberOfPlayers, int startingCoins, int smallBlind,
			int bigBlind) {
		Table.playersActors = new ActorRef[numberOfPlayers + 1];
		Table.players = new Player[numberOfPlayers + 1];
		Table.numberOfPlayers = numberOfPlayers;
		Table.startingCoins = startingCoins;
		Table.smallBlind = smallBlind;
		Table.bigBlind = bigBlind;
		Table.tableActor = this.getSelf();
		Table.deck = new Deck2();
		Table.playersBeingInGameInActualRound = numberOfPlayers;
		Table.playersInGameCounter = numberOfPlayers;
	}

	public static void join(final String name, WebSocket.In<JsonNode> in,
			WebSocket.Out<JsonNode> out) throws Exception {

		// Send the Join message to the table
		String result = (String) Await.result(
				ask(tableActor, new Join(name), 1000),
				Duration.create(1, SECONDS));

		if ("OK".equals(result)) {

			sitingPlayersCounter++;
			ActorRef playerRef = Akka.system().actorOf(
					Props.create(Human.class, name, in, out, tableActor,
							sitingPlayersCounter));
			playersActors[sitingPlayersCounter] = playerRef;
			players[sitingPlayersCounter] = new Player(name,
					sitingPlayersCounter, startingCoins, deck);

			String text = "my Actor has been added to Table";
			notifyAll(new Info(text, name));
			if (sitingPlayersCounter == numberOfPlayers) {
				whoDealer = Croupier.drawDealer(numberOfPlayers);
				try {
					setPlayersWithBlinds();
				} catch (GameException e) {
				}
				whoseTurn = Croupier
						.whichPlayerNext(whoseTurn, numberOfPlayers);
				playersBeingInGameInActualRound = playersInGameCounter;
				for (int i = 1; i < playersActors.length; i++)
					notifyOne(new CardInfo(players[i].getStringHand()), i);
				enableButtons(whoseTurn, isBet, false);
			}
		}
	}

	public static void quit(final String username) throws Exception {

		// to do
	}

	public void onReceive(Object message) throws Exception {

		if (message instanceof Join) {

			Join join = (Join) message;
			if(sitingPlayersCounter == numberOfPlayers) {
			    getSender().tell("NO", getSelf());
			}
			else {
				getSender().tell("OK", getSelf());
			}
		} else if (message instanceof ActionRequest) {
			String type = ((ActionRequest) message).getType();
			System.out.println(players[((ActionRequest) message).getNumber()].getName() + ": " + type);
			switch (type) {
			case "bet":
				try {
					updateBet(((ActionRequest) message).getNumber(),
							((ActionRequest) message).getValue());
				} catch (GameException e) {
					updateFold(((ActionRequest) message).getNumber(), true);
				}
				break;
			case "call":
				updateCall(((ActionRequest) message).getNumber());
				break;
			case "check":
				nextPlayerMove();
				break;
			case "raise":
				try {
					updateBet(((ActionRequest) message).getNumber(),
							((ActionRequest) message).getValue());
				} catch (GameException e) {
					updateFold(((ActionRequest) message).getNumber(), true);
				}
				break;
			case "fold":
				updateFold(((ActionRequest) message).getNumber(), false);
				break;
			case "exchange":
				try {
				    	String binCardSet = String.valueOf(((ActionRequest) message).getValue());
					exchangeCards(((ActionRequest) message).getNumber(),
							binCardSet);
				} catch (GameException e) {
					exchangeCards(((ActionRequest) message).getNumber(), "0000");
				}
				break;

			}

		} else if (message instanceof Number) {
			int nr = ((Number) message).getNumber();
			String name = ((Number) message).getName();
			getSender().tell(Deck.giveCard(nr), getSelf());
			String text = "I've sent    " + Deck.giveCard(nr).getRank() + " "
					+ Deck.giveCard(nr).getColor() + "    to " + name;
			notifyAll(new Info(text, "Table"));
		} else if (message instanceof Info) {
			String text = ((Info) message).getText();
			String name = ((Info) message).getName();

			notifyAll(new Info(text, name));
		} else if (message instanceof Quit) {

			String name = ((Quit) message).getName();
			int number = ((Quit) message).getNumber();
			updateFold(number, true);
			String text = "has left the game";
			notifyAll(new Info(text, name));

		} else {
			unhandled(message);
		}

	}

	static public void notifyAll(Object msg) {
		for (int i = 1; i <= sitingPlayersCounter; i++) {
			playersActors[i].tell(msg, tableActor);
		}
	}

	static public void notifyOne(Object msg, int playerNumber) {
		playersActors[playerNumber].tell(msg, tableActor);

	}
	public static boolean isPlaceForPlayer() {
	    return( sitingPlayersCounter < numberOfPlayers);
	}

	public static boolean isNameUsed(String name) {
		for (int i = 1; i <= sitingPlayersCounter; i++) {
			if (name.equals(players[i].getName()))
				return true;
		}
		return false;
	}

	public static void setPlayersWithBlinds() throws GameException {
		playerWithSmallBlind = Croupier.whichPlayerNext2(whoDealer, players,
				false);
		while (players[playerWithSmallBlind].getPlayersCoins() < smallBlind) {
			updateFold(playerWithSmallBlind, true);
			playerWithSmallBlind = Croupier.whichPlayerNext2(whoDealer,
					players, false);
		}
		playerWithSmallBlind = Croupier.whichPlayerNext2(whoDealer, players,
				false);
		players[playerWithSmallBlind].setBet(smallBlind);
		notifyAll(new Info("I have a small blind ("
				+ Integer.toString(smallBlind)+")",
				players[playerWithSmallBlind].getName()));
		players[playerWithSmallBlind].setPlayersCoins(players[playerWithSmallBlind].getPlayersCoins()-smallBlind);
		notifyAll(new AfterMoveInfo(smallBlind, smallBlind, players[playerWithSmallBlind].getPlayersCoins(), smallBlind, players[playerWithSmallBlind].getName()));
		int playerWithBigBlind = Croupier.whichPlayerNext2(
				playerWithSmallBlind, players, false);
		while (players[playerWithBigBlind].getPlayersCoins() < bigBlind) {
			updateFold(playerWithBigBlind, true);
			playerWithBigBlind = Croupier.whichPlayerNext2(
					playerWithSmallBlind, players, false);
		}
		players[playerWithBigBlind].setBet(bigBlind);
		maxBet = bigBlind;
		whoseTurn = playerWithBigBlind;
		notifyAll(new Info("I have a big blind (" + Integer.toString(bigBlind)+")",
				players[playerWithBigBlind].getName()));
		updateCoins(players[playerWithBigBlind], 0 - bigBlind);
		updateStake(smallBlind + bigBlind);
		notifyAll(new AfterMoveInfo(maxBet, stake, players[playerWithBigBlind].getPlayersCoins(), bigBlind, players[playerWithBigBlind].getName()));
	}

	public static void updateFold(int playerNr, boolean isPermament) {
		if (isPermament)
			players[playerNr].setStatus(Player.PLAYER_PERMAMENTLY_OUT_OF_GAME);
		else
			players[playerNr].setStatus(Player.PLAYER_OUT_OF_GAME);
		notifyAll(new Info("I folded", players[playerNr].getName()));
		playersInGameCounter--;
		playersFoldedCounter++;
		if (whoseTurn == playerNr)
			nextPlayerMove();
		else {
			playersBeingInGameInActualRound--;
			if (playersBeingInGameInActualRound < 2)
				nextPlayerMove();
		}

	}

	public static void updateCoins(Player player, int change) {
		int newCoins = player.getPlayersCoins() + change;
		player.setPlayersCoins(newCoins);
		notifyAll(new CoinsInfo(player.getName(), newCoins));
	}

	public static void enableButtons(int playerNr, boolean isBet,
			boolean addCheck) {
		int bet = players[playerNr].getBet();
		int coins = players[playerNr].getPlayersCoins();
		String possibleMoves = Croupier.possibleMoves(bet, coins, isBet,
				maxBet, round);
		if (addCheck)
			possibleMoves = "H" + possibleMoves;
		notifyAll(new Info("It is my turn!", players[playerNr].getName()));
		notifyOne(new MovePermission(possibleMoves), playerNr);
	}

	public static void nextPlayerMove() {
	    System.out.println("\nMOVE_COUNTER"+ moveCounter+"\nplayersFoldedCounter=" + playersFoldedCounter
			+ "\nplayersInActualGame=" + playersBeingInGameInActualRound
			+ "\nplayersExchangeCardsCounter="
			+ playersExchangedCardsCounter + "\nplayersInGameCounter="
			+ playersInGameCounter);
		if (playersFoldedCounter < (numberOfPlayers - 1)) {
			if (playersBeingInGameInActualRound > 1) { // chociaż dwóch graczy
				// nie dalo fold ani
				// all-in
				whoseTurn = Croupier.whichPlayerNext(whoseTurn, numberOfPlayers);
				// gdy wszyscy wymienili karty
				if (playersExchangedCardsCounter == (numberOfPlayers - playersFoldedCounter)) {
				    System.out.println("exch" + playersExchangedCardsCounter
					    + "    ,folded" + playersFoldedCounter);
				    
				    	updateRound();
				}
				// runda pierwsza i nikt przed osobą z dużą ciemną nie przebił
				// duzej ciemnej
				if (moveCounter == numberOfPlayers - 1 && round == 1
						&& Croupier.havePlayersTheSameBets(players)) {
				    	System.out.println("Dodatkowy check");
					enableButtons(whoseTurn, isBet, true); // dodatkowo może dać
					// check
					moveCounter++;
				}// POCZATEK WYMIANY wszyscy gracze wykonali mogli wykonać ruch
					// i gracze w grze mają te same zaklady
				else if (moveCounter >= playersBeingInGameInActualRound
						&& Croupier.havePlayersTheSameBets(players)) {
					if (round < 4) {
						if (playersExchangedCardsCounter == 0)
							playersBeingInGameInActualRound = playersInGameCounter;
						while (players[whoseTurn].getStatus() <= Player.PLAYER_OUT_OF_GAME) {
							whoseTurn = Croupier.whichPlayerNext(whoseTurn,
									numberOfPlayers);
						}
						System.out.println("Poczatek wymiany");
						notifyOne(new MovePermission("E"), whoseTurn);
						playersExchangedCardsCounter++;
					} else { // KONIEC GRY po 4 rundach- przynajmniej dwóch
						// graczy nie
						// dało fold ani all-in
						displayWinner();
						givePlayerTheirPrize();
						prepareNewDeal();
						System.out.println("Koniec pierwszego rozdania");
						startNewDeal();
					}
				} else { // Kontynuacja ruchów- rózne zakłady lub nie wszyscy
					// mogli
					// wykonać ruch
					while (players[whoseTurn].getStatus() != Player.PLAYER_IN_GAME) {
						whoseTurn = Croupier.whichPlayerNext(whoseTurn,
								numberOfPlayers);
					}
					enableButtons(whoseTurn, isBet, false);
					moveCounter++;
				}
			}// jest tylko jeden gracz, który nie dał fold ani all-in, i chociaż
				// jeden z all-in
			else {
			    	System.out.println("jest tylko jeden gracz, który nie dał fold ani all-in, i chociaż jeden z all-in");
			
				whoseTurn = Croupier.whichPlayerNext(whoseTurn, numberOfPlayers);
				// wszyscy wymienili karty
				if (playersExchangedCardsCounter == (numberOfPlayers - playersFoldedCounter)) {
					updateRound();
				}
				// trwa wymiana kart
				if (round < 4) {
					while (players[whoseTurn].getStatus() <= Player.PLAYER_OUT_OF_GAME)
						whoseTurn = Croupier.whichPlayerNext(whoseTurn,numberOfPlayers);
					System.out.println("Trwa wymiana");
					notifyOne(new MovePermission("E"), whoseTurn);
					playersExchangedCardsCounter++;
				} else {// KONIEC GRY
					displayWinner();
					givePlayerTheirPrize();
					prepareNewDeal();
					System.out.println("Koniec pierwszego rozdania");
					startNewDeal();
				}
			}
		} else {// Koniec GRY został tylko jeden gracz w grze(zwyciezca), reszta
			// dała
			// fold
			displayWinner();
			givePlayerTheirPrize();
			prepareNewDeal();
			System.out.println("Koniec pierwszego rozdania");
			startNewDeal();

		}
	}

	public static void displayWinner() {
		// TO DO class WinnerInfo may look the way you want
		String[] names = new String[numberOfPlayers];
		String[] cardRepresentations = new String[numberOfPlayers];
		for (int i = 1; i < players.length; i++) {
		    	names[i-1]= new String(players[i].getName());
			if (players[i].getStatus() <= Player.PLAYER_OUT_OF_GAME) {
				cardRepresentations[i-1]= new String("FOLDED");
			} else {
			    	cardRepresentations[i-1]= new String(players[i].getPlayersHand().getFormatedString());
			}
		}
		WinnerInfo winInfo = new WinnerInfo(names, cardRepresentations, getWinners());
		notifyAll(winInfo);

	}

	private static String getWinners() {
		String winners = "";
		int[][] Hands = new int[players.length][];

		for (int i = 1; i < players.length; i++) {
			Hands[i] = Hand.getReducedHand(players[i].getPlayersHand()
					.getCards());
		}
		int[] bestHand = { -1, -1, -1, -1 };
		for (int i = 1; i < players.length; i++) {
			if (players[i].getStatus() > Player.PLAYER_OUT_OF_GAME) {
				if (bestHand[0] != -1)
					bestHand = Hand.giveBetterHand(bestHand, Hands[i]);
				else
					bestHand = Hands[i];
			}
		}
		for (int i = 1; i < players.length; i++) {
		    	if (players[i].getStatus() > Player.PLAYER_OUT_OF_GAME) {
				if (!(bestHand.equals(Hands[i]))) {
					if (!(Hand.giveBetterHand(Hands[i], bestHand).equals(Hand
							.giveBetterHand(bestHand, Hands[i])))) {
					    	System.out.println("Player"+ i +players[i].getName());
						winners += players[i].getName() + ", ";
					} 
				} 
				else {
				    winners += players[i].getName() + ", "; 
				}
			}
		}
		System.out.println("Win: " +winners);
		return winners;

	}

	public static void givePlayerTheirPrize() {
		String winnersString = getWinners();
		int numberOfWinners = 0;
		for (int i = 0; i < winnersString.length(); i++) {
			if (Character.toString(winnersString.charAt(i)).equals("W")) {
				numberOfWinners++;
			}
		}
		if (numberOfWinners == 1) { // Only one winner. Bank doesn't take a
			// prize
			for (int i = 0; i < winnersString.length(); i++) {
				if (Character.toString(winnersString.charAt(i)).equals("W")) {
					if (players[i + 1].getStatus() != Player.PLAYER_PLAYED_ALLIN) { // regular
						// winner
						// (NOT
						// ALL
						// IN)
						updateCoins(players[i + 1], stake);
					} else { // player[i] played ALL IN
						int prize = 0;
						int allInBet = players[i + 1].getBet();
						for (int j = 1; j < players.length; j++) {
							if (allInBet <= players[j].getBet())
								prize += allInBet;
							else
								prize += players[j].getBet();
						}
						updateCoins(players[i + 1], prize);
					}
				}
			}
		}

	}

	public void updateBet(int playerNr, int bet) throws GameException {
		isBet = true;
		if (bet <= 0 && bet > players[playerNr].getPlayersCoins())
			throw new GameException("You send incorrect bet");
		int sum = bet + players[playerNr].getBet();
		if (sum > maxBet)
			maxBet = sum;
		players[playerNr].setBet(sum);
		players[playerNr].setPlayersCoins(players[playerNr].getPlayersCoins()
				- bet);
		updateStake(bet);
		notifyAll(new AfterMoveInfo(maxBet, stake,
				players[playerNr].getPlayersCoins(), sum,
				players[playerNr].getName()));
		nextPlayerMove();

	}

	public void updateCall(int playerNr) {
		isBet = true;
		int change = maxBet - players[playerNr].getBet();
		players[playerNr].setBet(maxBet);
		players[playerNr].setPlayersCoins(players[playerNr].getPlayersCoins()
				- change);
		updateStake(change);
		notifyAll(new AfterMoveInfo(maxBet, stake,
				players[playerNr].getPlayersCoins(), maxBet,
				players[playerNr].getName()));
		nextPlayerMove();
	}

	public static void updateStake(int betChange) {
		stake = stake + betChange;
	}

	public static void prepareNewDeal() {
	    	System.out.println("Preper new deal");
		int playersPermamentlyFoldedCounter = 0;
		for (int i = 1; i < players.length; i++) {// chyba trzeba zwrócić karty
			// gracza odłaczonego
			players[i].getPlayersHand().discardAllCards(deck);
			players[i].setBet(0);
			if (players[i].getStatus() != Player.PLAYER_PERMAMENTLY_OUT_OF_GAME
					&& players[i].getPlayersCoins() > 0) {
				players[i].setStatus(Player.PLAYER_IN_GAME);
			} else {
				players[i].setStatus(Player.PLAYER_PERMAMENTLY_OUT_OF_GAME);
				players[i].setBet(0);
				playersPermamentlyFoldedCounter++;
			}
		}
		deck.shuffle();

		if ((numberOfPlayers - playersPermamentlyFoldedCounter) < 2) {
			int i = 1;
			while (players[i].getStatus() != Player.PLAYER_IN_GAME) {
				i++;
			}
			for (int j = 1; j < players.length; j++) {
				notifyAll(new Info(players[i].getName()
						+ " won.\nThe game is over", "Table"));
			}
		} else {
			for (int i = 1; i < players.length; i++) {
				if (players[i].getStatus() != Player.PLAYER_PERMAMENTLY_OUT_OF_GAME) {
					players[i].getPlayersHand().drawAllCards(deck, i);
					/*
					 * for (int j = 1; j < players.length; j++) { if
					 * (playersActors[j].isWorking())
					 * playersActors[j].displayBet("0", players[i].getName()); }
					 */
				}
			}
			round = 1;
			stake = 0;
			isBet = false;
			playersFoldedCounter = playersPermamentlyFoldedCounter;
			playersInGameCounter = numberOfPlayers
					- playersPermamentlyFoldedCounter;
			playersExchangedCardsCounter = 0;
			playersBeingInGameInActualRound = playersInGameCounter;
			try {
				whoDealer = Croupier
						.whichPlayerNext2(whoDealer, players, false);
			} catch (GameException e) {
			}
			System.out.println("Po prepareNewDeal ->New round");
			notifyAll(new Info("Round " + Integer.toString(round), "Table"));
		}
	}

	

	public static void updateRound() {
		round++;
		moveCounter = 0;
		isBet = false;
		playersExchangedCardsCounter = 0;
		playersBeingInGameInActualRound = playersInGameCounter;
		whoseTurn = playerWithSmallBlind;
		System.out.println("Update Round");
		notifyAll(new Info("Round " + Integer.toString(round), "Table"));
	}

	public void exchangeCards(int playerNr, String binCardSet) throws GameException {
	    	try {
        	    	int cardSet = Integer.parseInt(binCardSet, 2);
        	    	if (cardSet > 15 || cardSet < 0)
        			throw new GameException(
        					"You tried exchange more cards than you had had");
        		if (cardSet > 0) {
        			int exchangedCardsCounter = 0;
        			String binaryCardSet = Integer.toBinaryString(cardSet);
        			for (int i = 0; i < binaryCardSet.length(); i++) {
        				if (binaryCardSet.charAt(i) == '1')
        					exchangedCardsCounter++;
        			}
        			players[playerNr].exchangeCards(deck, cardSet);
        			notifyOne(new CardInfo(players[playerNr].getStringHand()), playerNr);
        			notifyAll(new Info(players[playerNr].getName() + "exchanged "
        					+ exchangedCardsCounter + " cards", "Table"));
        		}
        		nextPlayerMove();
	    	} catch (NumberFormatException e) {
	    	    throw new GameException("You send wrong cardSet");
	    	}
	}
	
	public static void startNewDeal() {
		try {
		    setPlayersWithBlinds();
		    playersBeingInGameInActualRound = playersInGameCounter;
		    whoseTurn = Croupier.whichPlayerNext2(whoseTurn, players, false);
		    for (int i = 1; i < players.length; i++)
			if (players[i].getStatus() != Player.PLAYER_PERMAMENTLY_OUT_OF_GAME) {
			    notifyOne(new CardInfo(players[i].getStringHand()), i);
			}
		    moveCounter = 1;
		    enableButtons(whoseTurn, isBet, false);
		} catch (GameException e) {
		    int i = 1;
		    while (players[i].getStatus() != Player.PLAYER_IN_GAME) {
			i++;
		    }
		    for (int j = 1; j < players.length; j++) {
			if (j == i)
			    notifyOne(new Info("You won. The game is over", "Table"), j);
			else
			    notifyOne(new Info (players[i].getName() + " won. The game is over", "Table"), j);
		    }
		    // System.exit(1);
		}
	    }
}
